یکی از این الگوها، استفاده از RIGHT OUTER JOIN است.
بیایید از ابتدا روشن کنیم:
RIGHT OUTER JOIN نه اشتباه است، نه ممنوع، و نه لزوماً کندتر از LEFT OUTER JOIN. اما در اغلب کوئری های دست نویس، انتخابی است که خواندن و نگهداری کد را سخت تر می کند؛ آن هم بدون اینکه مزیت مشخصی به همراه داشته باشد.
مسئله چیست؟ RIGHT JOIN که کاملاً قانونی است!
بله، کاملاً قانونی است. SQL Server از RIGHT OUTER JOIN پشتیبانی می کند و موتور بهینه ساز نیز معمولاً می تواند آن را به شکل منطقی معادل با یک LEFT OUTER JOIN پردازش کند.
برای مثال، این کوئری را در نظر بگیرید:
SELECT c.CustomerID, c.CustomerName, SUM(o.TotalAmount) AS OrderTotal
FROM dbo.Orders AS o
RIGHT OUTER JOIN dbo.Customers AS c ON c.CustomerID = o.CustomerID
GROUP BY c.CustomerID, c.CustomerName; هدف کوئری این است:
همه مشتریان را نمایش بده؛ اگر سفارشی دارند، مجموع مبلغ سفارش هایشان را هم نشان بده.
نسخهٔ معادل و معمولاً خواناتر آن چنین است:
SELECT c.CustomerID, c.CustomerName, SUM(o.TotalAmount) AS OrderTotal
FROM dbo.Customers AS c
LEFT OUTER JOIN dbo.Orders AS o ON c.CustomerID = o.CustomerID
GROUP BY c.CustomerID, c.CustomerName; در بسیاری از موارد، این دو کوئری از نظر نتیجه و پلن اجرایی تفاوت معناداری ندارند. تفاوت اصلی در این است که نسخه دوم برای انسان ها واضح تر است.
انسان ها کوئری را از چپ به راست می خوانند
اغلب ما هنگام خواندن SQL، ابتدا به FROM نگاه می کنیم و سپس مسیر ارتباط جدول ها را دنبال می کنیم.
در نسخهٔ LEFT OUTER JOIN، روایت کوئری طبیعی است:
- از مشتریان شروع کن؛
- همه مشتریان را نگه دار؛
- اگر سفارش مرتبطی وجود دارد، آن را هم اضافه کن.
FROM dbo.Customers AS c
LEFT OUTER JOIN dbo.Orders AS o ON c.CustomerID = o.CustomerID اما در RIGHT OUTER JOIN، ابتدا از جدول سفارش ها شروع می کنید و بعد متوجه می شوید که جدول مهم تر—یعنی مشتریان—در سمت راست قرار دارد:
FROM dbo.Orders AS o
RIGHT OUTER JOIN dbo.Customers AS c ON c.CustomerID = o.CustomerID در این وضعیت، خواننده باید در ذهنش جای دو جدول را عوض کند تا منطق اصلی را بفهمد. این کار شاید برای یک کوئری کوتاه چند ثانیه زمان ببرد، اما در کوئری های واقعی با چندین Join، CTE، فیلتر و Aggregate، همین بار ذهنی کوچک می تواند منشأ سوءبرداشت و خطا شود.
جدول «محفوظ» را واضح نمایش دهید
از منظر منطقی، در Outer Join یک طرف از Join حفظ می شود:
- در
LEFT OUTER JOINهمه رکوردهای جدول سمت چپ حفظ می شوند. - در
RIGHT OUTER JOINهمه رکوردهای جدول سمت راست حفظ می شوند.
در بیشتر سناریوهای کسب وکاری، بهتر است جدولی که قرار است تمام رکوردهای آن حفظ شود، در ابتدای زنجیرهٔ Joinها قرار گیرد.
برای نمونه:
- گزارش همه مشتریان، حتی بدون سفارش →
Customers LEFT JOIN Orders - گزارش همه محصولات، حتی بدون فروش →
Products LEFT JOIN Sales - گزارش همه کارکنان، حتی بدون ثبت حضور →
Employees LEFT JOIN Attendance
این الگو باعث می شود «موضوع اصلی گزارش» در همان ابتدای کوئری مشخص باشد.
FROM dbo.Products AS p
LEFT OUTER JOIN dbo.Sales AS s ON s.ProductID = p.ProductID با دیدن این بخش، حتی بدون خواندن کامل SELECT، می دانیم که خروجی باید شامل تمام محصولات باشد.
مشکل اصلی: عملکرد نیست، نگهداری پذیری است
یکی از اشتباهات رایج در بحث Joinها این است که فوراً سراغ Performance برویم. در حالی که موضوع RIGHT OUTER JOIN در اغلب موارد، بیشتر یک مسئلهٔ خوانایی و قابلیت نگهداری است تا Performance.
در SQL Server، بهینه ساز معمولاً می تواند ترتیب منطقی Joinها را بازنویسی و بهترین پلن را بر اساس آمار، ایندکس ها، حجم داده و Predicateها انتخاب کند. بنابراین صرف تبدیل RIGHT JOIN به LEFT JOIN نباید به عنوان یک تکنیک تضمینی برای افزایش سرعت مطرح شود.
اما تبدیل آن معمولاً مزیت های مهم تری دارد:
- فهم سریع تر هدف کوئری
- کاهش احتمال تغییر اشتباه در آینده
- ساده تر شدن Debugging
- آسان تر شدن آموزش اعضای جدید تیم
- یکدستی بیشتر در Code Review
- خواناتر شدن زنجیره های Join
در یک پروژه کوچک شاید این موارد کم اهمیت به نظر برسند؛ اما در یک کدبیس بزرگ که ده ها یا صدها Stored Procedure، View و Report دارد، این تفاوت ها به مرور بسیار اثرگذار می شوند.
وقتی RIGHT JOIN نشانهٔ پیچیدگی پنهان است
دیدن یک RIGHT OUTER JOIN به تنهایی به معنای بد بودن کوئری نیست. اما گاهی نشان می دهد نویسنده از ابتدا جدول اصلی را درست انتخاب نکرده یا مسیر Joinها را در طول توسعه تغییر داده است.
این مسئله در کوئری های زنجیره ای بیشتر دیده می شود:
FROM dbo.TableA AS a
INNER JOIN dbo.TableB AS b ON b.AID = a.AID
LEFT OUTER JOIN dbo.TableC AS c ON c.BID = b.BID
INNER JOIN dbo.TableD AS d ON d.CID = c.CID
RIGHT OUTER JOIN dbo.TableE AS e ON e.DID = d.DID چنین ساختاری لزوماً نادرست نیست، اما باید با دقت بررسی شود:
- جدول محوری خروجی دقیقاً کدام است؟
- کدام موجودیت ها باید حتی در نبود رابطهٔ مرتبط باقی بمانند؟
- آیا یک
INNER JOINبعد ازLEFT JOINعملاً Outer Join را به Inner Join تبدیل کرده است؟ - آیا می توان منطق را با CTE یا زیرکوئری های هدفمندتر ساده کرد؟
- آیا ترتیب Joinها با نیاز کسب وکار هم خوان است؟
اگر بازنویسی RIGHT JOIN به LEFT JOIN دشوار باشد، احتمالاً مشکل فقط نوع Join نیست؛ بلکه طراحی کلی کوئری نیاز به بازنگری دارد.
مراقب شرط های WHERE بعد از LEFT JOIN باشید
یکی از مواردی که در بازنویسی Joinها باید با دقت بررسی شود، محل قرار گرفتن شرط هاست. فرض کنید می خواهیم تمام مشتریان را نگه داریم، اما فقط سفارش های پرداخت شده را نمایش دهیم.
نسخهٔ صحیح:
SELECT c.CustomerID, c.CustomerName, o.OrderID, o.TotalAmount
FROM dbo.Customers AS c
LEFT OUTER JOIN dbo.Orders AS o ON o.CustomerID = c.CustomerID AND o.Status = N'Paid'; اما اگر شرط جدول سمت راست را در WHERE قرار دهیم:
SELECT c.CustomerID, c.CustomerName, o.OrderID, o.TotalAmount
FROM dbo.Customers AS c
LEFT OUTER JOIN dbo.Orders AS o ON o.CustomerID = c.CustomerID
WHERE o.Status = N'Paid'; در عمل، مشتریانی که سفارش ندارند حذف می شوند؛ زیرا مقدار o.Status برای آن ها NULL است و شرط WHERE را پاس نمی کنند. در این شرایط، LEFT OUTER JOIN عملاً رفتار نزدیک به INNER JOIN پیدا می کند.
بنابراین، در Code Review فقط نباید پرسید «چرا RIGHT JOIN استفاده شده؟»؛ بلکه باید منطق حفظ رکوردها و محل اعمال Predicateها را نیز بررسی کرد.
آیا باید OUTER را صریح بنویسیم؟
از نظر SQL Server، این دو عبارت یکسان هستند:
LEFT JOIN
LEFT OUTER JOIN کلمهٔ OUTER اختیاری است. با این حال، نوشتن صریح آن می تواند در کدهای تیمی خوانایی را بیشتر کند:
INNER JOINLEFT OUTER JOIN در نگاه اول، تفاوت میان Join داخلی و خارجی واضح تر می شود. این موضوع به ویژه در کوئری های بلند و چندجدولی مفید است.
البته مهم تر از انتخاب بین LEFT JOIN و LEFT OUTER JOIN، داشتن یک استاندارد ثابت در کل پروژه است. تیم می تواند هرکدام را انتخاب کند، اما بهتر است همان سبک را در تمام Stored Procedureها، Viewها و اسکریپت ها حفظ کند.
آیا هیچ وقت نباید از RIGHT OUTER JOIN استفاده کرد؟
خیر. استفاده از RIGHT OUTER JOIN ممنوع نیست. در برخی شرایط، به خصوص در SQL تولیدشده توسط ابزارها، Query Generatorها یا فرایندهای تبدیل خودکار کوئری، ممکن است کاملاً طبیعی و قابل قبول باشد.
همچنین اگر کل تیم آگاهانه این سبک را انتخاب کرده و همه با آن راحت هستند، مشکل جدی ایجاد نمی شود.
اما در کدنویسی دستی، یک پرسش ساده ارزشمند است:
آیا این
RIGHT OUTER JOINواقعاً خواناتر از نسخهٔ معادل باLEFT OUTER JOINاست؟
در بیشتر موارد پاسخ منفی است.
یک استاندارد پیشنهادی برای تیم های SQL Server
برای کاهش ابهام و ساده تر شدن Code Review، می توان این قواعد را در راهنمای کدنویسی تیم قرار داد:
- در کوئری های دست نویس،
LEFT OUTER JOINرا بهRIGHT OUTER JOINترجیح دهید. - موجودیت اصلی خروجی را در سمت چپ و ابتدای عبارت
FROMقرار دهید. - نوع Join را بر اساس نیاز واقعی به حفظ رکوردها انتخاب کنید، نه صرفاً برای رسیدن به خروجی فعلی.
- شرط های مربوط به جدول اختیاری را با دقت در
ONیاWHEREقرار دهید. - برای همه Joinها از یک سبک نام گذاری، Alias و قالب بندی ثابت استفاده کنید.
- کوئری های چند Joinی و پیچیده را با CTE، View یا مراحل منطقی کوچک تر قابل فهم تر کنید.
سید حامد واحدی
30 مرداد 1405